//#define DEBUG
#include <getopt.h>
#include <unistd.h>
#include <time.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <iomanip>

#include "LogicPool/LogicInterfaceUSB.h"
#include "LogicPool/DFM.h"
#include "Profile.h"
#include "teebuf.h"

using namespace std;

char * log_fn     = (char *)"dfm.log";
char * dfm_ini    = (char *)"dfm.ini";
#ifndef DEBUG
char * dfm_device = (char *)"/dev/ttyUSB0";
#else
char * dfm_device = (char *)"/dev/null";
#endif
char DFM_APP[]    = "DFM";
const int DEFAULT_LIMIT = -1;
const int DEFAULT_TIME  = -3;


char * timestr();


int main(int argc, char ** argv) 
{
    // hardware access
    LogicInterfaceUSB * interface;
    DFM * dfm;
    
    // options
    Profile profile(dfm_ini);
    bool reset_alarm = false;
    bool init_device = false;
    bool monitor_device = false;
    char opt;
#ifdef DEBUG
    bool have_device = true;
#else
    bool have_device = false;
#endif
    unsigned time_interval = 5;
    bool have_interval = false;

    // hardware settings
    unsigned limit[8];
    unsigned reaction_time = 0;
    unsigned id;

    // misc
    int channel;
    int rc = 0;
    clock_t last_access;
#ifdef DEBUG    
    int grace_time = -5;
#else
    int grace_time = 2;
#endif    

    // write to string buffer
    stringstream tmplog;
    streambuf * original_cerr_sb = cerr.rdbuf(); // save original readbuffer of cerr;
    cerr.rdbuf( new teebuf(original_cerr_sb, tmplog.rdbuf()) );
       
    // separator for log file
    tmplog << endl;
    tmplog << "--------------------------------------------------------------" << endl;
    tmplog << endl;
    
    cerr << "DFM control 1.0"
         << "  revision: " << SVN_REV << ", " 
         << "built: " << __DATE__ << " " << __TIME__ << endl << endl;
    
    while ((opt = getopt(argc, argv, "c:d:hilmrt:")) != -1) {
        switch (opt) {
        case 'c':
            dfm_ini = strdup(optarg);
            profile.SetFileName(dfm_ini);
            break;
        case 'd':
            dfm_device = strdup(optarg);
            have_device = true;
            break;
        case 'l':
            log_fn = strdup(optarg);
            break;
        case 'i':
            init_device = true;
            break;          
        case 'm':
            monitor_device = true;
            break;          
        case 't':
            time_interval = atoi(optarg);
            have_interval = true;
            break;
        case 'r':
            reset_alarm = true;
            break;
        case 'h':
        default:
            cout << "Usage: " << argv[0] << "[-c <file>] [-d <device>] [-l <log>] [-i] [-r] [-m] [-t <time>]" << endl;
            cout << "Options:" << endl;
            cout << "     " << "-c <file>   select configuration file" << endl;
            cout << "     " << "-d <device> select USB device" << endl;
            cout << "     " << "-l <log>    select log file" << endl;
            cout << "     " << "-i          initialize device from config file" << endl;
            cout << "     " << "-r          reset device" << endl;
            cout << "     " << "-m          monitor device" << endl;
            cout << "     " << "-t <time>   interval between successive monitoring requests" << endl;
            exit(EXIT_FAILURE);
        };
    }

    // open log file
    ofstream logs((char *)log_fn, ios_base::app | ios_base::out);
    if (!logs) {
        cerr << "ERROR: could not open log file \"" << log_fn << "\"" << endl;
    }
    
    // copy tmplog to log file
    logs << tmplog.str();
    
    // from now on write directly to log file
    cerr.rdbuf(new teebuf(original_cerr_sb, logs.rdbuf()));

    // timestamp
    cerr << "started at: " << timestr() << endl;

    // output this to stdout only
    cout << "saving all output to log file: " << log_fn << endl;

    // read device name from profile
    if (!have_device)
        dfm_device = (char *)profile.GetString(DFM_APP, "Device", dfm_device).c_str();

    // open the USB interface
    cerr << "opening device " << dfm_device << ", ";
    interface = new LogicInterfaceUSB(dfm_device);
    interface->Open();
    // FIXME: should actually check if dfm_device is actually accessible
    
    // Check ID; abort if we get the wrong ID
    dfm = new DFM(*interface, true);
    cerr << "got DFM box ID: " << hex << (id = dfm->GetID());
#   ifdef DEBUG
    id = 2;
#   endif
    if (id == 2) 
        cerr << ", ok" << endl;
    else {
        cerr << endl << "ERROR: something is wrong: ID should be 2!" << endl;
        logs << flush;
        exit(-2);
    }

    //TODO: check if profile actually exists
  
	// Init number of pulses before doing anything else to get correct readings ;-)
	
	// first, read number of pulses per liter (default)
    double pulses = profile.GetDouble(DFM_APP, "Pulses", 0.);
    if (pulses != 0.)
    	dfm->SetPulses(pulses);
    
	// read number of pulses per liter (per channel settings)
    for (channel = 0; channel < 8; channel ++) {
        stringstream sstr;
        sstr << "Pulses" << channel + 1;
        string pulsesstr = sstr.str();
        double pulses = profile.GetDouble(DFM_APP, pulsesstr, 0.);
        if (pulses != 0.) 
        	dfm->SetPulses(channel, pulses);
	}

	// log pulse settings
	cerr << "pulses:";
    for (channel = 0; channel < 8; channel ++)
    	cerr << " " << fixed << setprecision(0) << dfm->GetPulses(channel);
    cerr << endl;
    
    // read current settings from DFM
    cerr << "reading current limits from device:";
    for (channel = 0; channel < 8; channel++) {
        limit[channel] = dfm->GetLimit(channel);
        cerr << " " << dec << fixed << setprecision(1) << dfm->Flow(channel, limit[channel]);
    }
    cerr << endl;
    
    if (!init_device) {
        // reaction time is lost during reset, so read it from profile
        cerr << "reading only reaction time from " << dfm_ini << endl;
        reaction_time = profile.GetInt(DFM_APP, "ReactionTime", DEFAULT_TIME);
        dfm->SetReactionTime(reaction_time);

        // write back limits
        for (channel = 0; channel < 8; channel++)
            dfm->SetLimit(channel, limit[channel]);
    } else {
        cerr << "reading settings from " << dfm_ini << endl;

        // read and set limits drom profile
        cerr << "setting limits: ";
        for (channel = 0; channel < 8; channel ++) {
            stringstream sstr;
            sstr << "Limit" << channel + 1;
            string limitstr = sstr.str();

            limit[channel] = dfm->Pulses(channel, profile.GetDouble(DFM_APP, limitstr, DEFAULT_LIMIT));
            dfm->SetLimit(channel, limit[channel]);
            
            cerr << " " << fixed << setprecision(1) << dfm->Flow(channel, limit[channel]);
        }
        cerr << endl;
        
        // read and set reaction time from profile
        reaction_time = profile.GetInt(DFM_APP, "ReactionTime", DEFAULT_TIME);
        dfm->SetReactionTime(reaction_time);
        cerr << "setting reaction time to " << (int)reaction_time << "s." << endl;
    }

    if (reset_alarm) {
        cerr << "resetting pending alarms" << endl; 
        dfm->Reset();
    }

    // verify all limits
    cerr << "verifying limits...";
    for (channel = 0; channel < 8; channel ++) {
        unsigned read_limit = dfm->GetLimit(channel);
        if (limit[channel] != read_limit) {
            cerr << endl 
                << "ERROR: limit read back from device (" << read_limit << ") for channel " << channel + 1 
                << " differs from value set! (" << limit[channel] << ")";
            rc = -1;
        }
    }
    if (rc == 0)
        cerr << " ok!";
    cerr << endl;

    last_access = clock();
    
    if (monitor_device) {

        // read time interval from profile
        if (!have_interval)
            time_interval = profile.GetInt(DFM_APP, "MonitorInterval", time_interval);

        // first second is always unreliable after reset
        if (reset_alarm)
            sleep(1);
            
        cerr << "reading current flow [l/min] and alarm state every " << time_interval << " seconds" << endl;
        while (true) {

            // if the box gets turned on anf off, we get an access "timeout"
            //cout << "last " << last_access <<
            if (clock() > (last_access + CLOCKS_PER_SEC * (signed(time_interval) + grace_time))) {
                cerr << "WARNING: lost contact to DFM box at " << timestr() << ", ";
                cerr << "setting reaction time: " << reaction_time << ", ";
                dfm->SetReactionTime(reaction_time); 
                cerr << "setting limits: ";
                for (channel = 0; channel < 8; channel++) {
                    dfm->SetLimit(channel, limit[channel]);
                    cerr << " " << fixed << setprecision(1) << dfm->Flow(channel, limit[channel]);
                }
                cerr << endl;
            }
            
            // get current time
            cerr << timestr();
            
            // get current flow rates
            cerr << "\t";
            double total_flow = 0;
            for (channel = 0; channel < 8; channel++) {
                double flow = dfm->GetFlow(channel);
                total_flow += flow; // accumulate flow
                cerr << " ";
                cerr.width(5);
                cerr << fixed << setprecision(1) << flow;
            }
            cerr << "\t";
            cerr.width(5);
            cerr << fixed << setprecision(1) << total_flow;
    
            // get alarm state
            cerr << "\t";
            for (channel = 0; channel < 8; channel++) {
                unsigned state = dfm->GetState(channel);
                cerr.width(1);
                cerr << " " << state;
            }
            
            // flush output
            cerr << endl;
            logs << flush;
            
            // remember last hw access time
            last_access = clock();
            
            // wait
            sleep(time_interval);
        }
    }
    
    // close interface
    interface->Close();
    
    return rc;
}


char * timestr()
{
    static char timestr[1024];
    time_t now = time(NULL);
    strftime(timestr, sizeof(timestr), "%y-%m-%d %H:%M:%S", localtime(&now));
    return timestr;
}
